﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using MyUtils.Properties;
using System.Diagnostics;

namespace MyUtils
{
#if !PocketPC && !Smartphone
	/// <summary>
	/// Denotes a length of information in bytes.
	/// It is easy to display 5.5 MiB or parse 3.4 KiB
	/// </summary>
	[DebuggerDisplay("{DebuggerDisplay}")]
	[TypeConverter(typeof(InfoSizeTypeConverter))]
#endif
	public struct InfoSize : IEquatable<InfoSize>, IComparable, IComparable<InfoSize>, IFormattable
	{
		// ReSharper disable UnusedMember.Local
		string DebuggerDisplay
		{
			get { return ToString("{1:0.###} {4} ({0:#,0} bytes)"); }
		}
// ReSharper restore UnusedMember.Local

		public static Format DefaultPersistentFormat;
		public static Format DefaultUiFormat;

		public enum Format
		{
			/// <summary>
			/// IEC
			/// </summary>
			Default,

			/// <summary>
			/// correct: 5.5 MiB = 5.5 * 2^20 bytes
			/// </summary>
			Iec,

			/// <summary>
			/// SI format: 5.5 MB = 5.5 * 10^(3*2) bytes
			/// </summary>
			Si,

			/// <summary>
			/// legacy: 5.5 MB = 5.5 * 2^20 bytes
			/// </summary>
			BinarySi,
		}

		readonly long _size;

		public InfoSize(long length)
		{
			_size = length;
			SelfTest();
		}

		[Conditional("DEBUG")]
		void SelfTest()
		{
#if DEBUG && !PocketPC
			if (_insideSelfTest > 0)
			{
				return;
			}
			try
			{
				_insideSelfTest++;
				var str = ToString("{1} {4}");
				if(str.Length<30)
				{
					(this == Parse(str)).Ensure();
				}
			}
			finally
			{
				_insideSelfTest--;
			}
#endif
		}

#if DEBUG && !PocketPC
		[ThreadStatic]
		static int _insideSelfTest;
#endif

		public static implicit operator InfoSize(int length)
		{
			return new InfoSize(length);
		}

		[CLSCompliant(false)]
		public static implicit operator InfoSize(uint length)
		{
			return new InfoSize(length);
		}

		public static implicit operator InfoSize(long length)
		{
			return new InfoSize(length);
		}

		public static implicit operator InfoSize(decimal length)
		{
			checked
			{
				return new InfoSize((long)length);
			}
		}

		[CLSCompliant(false)]
		public static implicit operator InfoSize(ulong length)
		{
			checked
			{
				return new InfoSize((long)length);
			}
		}

		[CLSCompliant(false)]
		public static implicit operator ulong(InfoSize infoSize)
		{
			return checked ((ulong)infoSize._size);
		}

		public static implicit operator long(InfoSize infoSize)
		{
			return checked((long)infoSize._size);
		}

		public static implicit operator decimal (InfoSize infoSize)
		{
			return infoSize._size;
		}

		[CLSCompliant(false)]
		public static implicit operator uint(InfoSize infoSize)
		{
			return checked((uint)infoSize._size);
		}

		public static implicit operator int(InfoSize infoSize)
		{
			return checked((int)infoSize._size);
		}

		/// <summary>
		/// CAUTION: implicitly converts from invariant culture with DefaultPersistentFormat
		/// Is is so useful from code beauty point of view, but so harmful for people who does not care or does not aware about UI, Cultures and Serialization Formatting
		/// </summary>
		public static implicit operator string(InfoSize infoSize)
		{
			// TODO populate a warning disclammer
			return infoSize.ToStringInvariant();
		}

		public string ToStringInvariant()
		{
			switch (DefaultPersistentFormat)
			{
				case Format.Default:
				case Format.Iec:
					return ToStringIec(null, CultureInfo.InvariantCulture);
				case Format.Si:
					return ToStringSi(null, CultureInfo.InvariantCulture);
				case Format.BinarySi:
					return ToStringBinarySi(null, CultureInfo.InvariantCulture);
				default:
					throw new ArgumentOutOfRangeException();
			}
		}

		/// <summary>
		/// CAUTION: implicitly converts from invariant culture with DefaultPersistentFormat
		/// Is is so useful from code beauty point of view, but so harmful for people who does not care or does not aware about UI, Cultures and Serialization Formatting
		/// </summary>
		public static implicit operator InfoSize(string str)
		{
			return ParseInvariant(str);
		}

		public static InfoSize ParseInvariant(string str)
		{
			switch (DefaultPersistentFormat)
			{
				case Format.Default:
				case Format.Iec:
					return ParseSizeString(str, CultureInfo.InvariantCulture, true);
				case Format.Si:
					return ParseSizeString(str, CultureInfo.InvariantCulture, false);
				case Format.BinarySi:
					return ParseSizeString(str, CultureInfo.InvariantCulture, true);
				default:
					throw new ArgumentOutOfRangeException();
			}
		}

		public static InfoSize Parse(string str)
		{
			return Parse(str, null);
		}

		public static InfoSize Parse(string str, IFormatProvider provider)
		{
			return Parse(str, provider, null);
		}

		public static InfoSize Parse(string str, IFormatProvider provider, Format? format)
		{
			switch (format ?? DefaultUiFormat)
			{
				case Format.Default:
				case Format.Iec:
					return ParseSizeString(str, provider, true);
				case Format.Si:
					return ParseSizeString(str, provider, false);
				case Format.BinarySi:
					return ParseSizeString(str, provider, true);
				default:
					throw new ArgumentOutOfRangeException();
			}
		}

		public static bool TryParse(string str, out InfoSize infoSize)
		{
			return TryParse(str, out infoSize, null);
		}

		public static bool TryParse(string str, out InfoSize infoSize, IFormatProvider provider)
		{
			// todo eliminate try catch
			try
			{
				infoSize = Parse(str, provider);
				return true;
			}
			catch
			{
				infoSize = default(InfoSize);
				return false;
			}
		}

		public bool Equals(InfoSize other)
		{
			return other._size == _size;
		}

		public int CompareTo(InfoSize other)
		{
			return other._size.CompareTo(_size);
		}

		/// <summary>
		/// To size string in current culture
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			return ToString(null, null);
		}

		string IFormattable.ToString(string format, IFormatProvider provider)
		{
			return ToString(format, provider, Format.Default);
		}

		/// <summary>
		/// To size string
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <returns></returns>
		public string ToString(string format)
		{
			return ToString(format, null);
		}

		/// <summary>
		/// To size string
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <param name="provider"></param>
		/// <returns></returns>
		public string ToString(string format, IFormatProvider provider)
		{
			return ToString(format, provider, null);
		}

		/// <summary>
		/// To size string
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <param name="provider"></param>
		/// <param name="sizeFormat"></param>
		/// <returns></returns>
		public string ToString(string format, IFormatProvider provider, Format? sizeFormat)
		{
			switch (sizeFormat ?? DefaultUiFormat)
			{
				case Format.Default:
				case Format.Iec:
					return ToStringIec(format, provider);
				case Format.Si:
					return ToStringSi(format, provider);
				case Format.BinarySi:
					return ToStringBinarySi(format, provider);
				default:
					throw new ArgumentOutOfRangeException();
			}
		}

		/// <summary>
		/// Binary power prefix with IEC prefix: 1024 Bytes = 1 * 2^10 = 1MiB
		/// Correct usage.
		/// </summary>
		/// <returns></returns>
		public string ToStringIec()
		{
			return ToStringIec(null);
		}

		/// <summary>
		/// Binary power prefix with IEC prefix: 1024 Bytes = 1 * 2^10 = 1MiB
		/// Correct usage.
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <returns></returns>
		public string ToStringIec(string format)
		{
			return ToStringIec(format, null);
		}

		/// <summary>
		/// Binary power prefix with IEC prefix: 1024 Bytes = 1 * 2^10 = 1MiB
		/// Correct usage.
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <param name="provider"></param>
		/// <returns></returns>
		public string ToStringIec(string format, IFormatProvider provider)
		{
			return ToSizeString(_size, format, provider, Resources.InfoSize_iB, true);
		}

		/// <summary>
		/// Binary power with SI prefix: 1024 Bytes = 1 * 2^10 = 1MB
		/// Historically used for RAM capacity and Windows file systems.
		/// </summary>
		/// <returns></returns>
		public string ToStringBinarySi()
		{
			return ToStringBinarySi(null);
		}

		/// <summary>
		/// Binary power with SI prefix: 1024 Bytes = 1 * 2^10 = 1MB
		/// Historically used for RAM capacity and Windows file systems.
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <returns></returns>
		public string ToStringBinarySi(string format)
		{
			return ToStringBinarySi(format, null);
		}

		/// <summary>
		/// Binary power with SI prefix: 1024 Bytes = 1 * 2^10 = 1MB
		/// Historically used for RAM capacity and Windows file systems.
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <param name="provider"></param>
		/// <returns></returns>
		public string ToStringBinarySi(string format, IFormatProvider provider)
		{
			return ToSizeString(_size, format, provider, Resources.InfoSize_B, true);
		}

		int IComparable.CompareTo(object obj)
		{
			if(obj is InfoSize)
			{
				var ins = (InfoSize) obj;
				return CompareTo(ins);
			}
			return 1;
		}

		/// <summary>
		/// Decimal power with usual SI prefix: 1024 Bytes = 1.024 * 10^3 = 1.024MB
		/// Historically used fo HDD capacity and iOS file system.
		/// </summary>
		/// <returns></returns>
		public string ToStringSi()
		{
			return ToStringSi(null);
		}

		/// <summary>
		/// Decimal power with usual SI prefix: 1024 Bytes = 1.024 * 10^3 = 1.024MB
		/// Historically used fo HDD capacity and iOS file system.
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <returns></returns>
		public string ToStringSi(string format)
		{
			return ToStringSi(format, null);
		}

		/// <summary>
		/// Decimal power with usual SI prefix: 1024 Bytes = 1.024 * 10^3 = 1.024MB
		/// Historically used fo HDD capacity and iOS file system.
		/// </summary>
		/// <param name="format">"{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}"</param>
		/// <param name="provider"></param>
		/// <returns></returns>
		public string ToStringSi(string format, IFormatProvider provider)
		{
			return ToSizeString(_size, format, provider, Resources.InfoSize_B, false);
		}

		static string ToSizeString(decimal size, string format, IFormatProvider provider, string postfix, bool binaryPower)
		{
			var original = size;

			var negated = false;
			if (size < 0)
			{
				size = decimal.Negate(size);
				negated = true;
			}

			var scale = 0;
			var pow = binaryPower ? 1024M : 1000M;
			while (size >= pow)
			{
				scale++;
				size = decimal.Divide(size, pow);
			}

//			var tune = decimal.Floor(size * 10);
//			if (tune < size * 10)
//			{
//				tune++;
//			}
//			size = tune / 10;
			//size = decimal.Round(size, 1, MidpointRounding.AwayFromZero);

			if (negated)
			{
				size = decimal.Negate(size);
			}

			var suff = GetSizeSuffix(scale);
			var suffPostf = GetSizeSuffix(scale, postfix, binaryPower);

			return string.Format(provider, format ?? "{1:0.#} {4}", original, size, suff, string.IsNullOrEmpty(suff)?string.Empty: postfix, suffPostf); // size.ToString(format ?? _defaultFormat, provider) + " " + GetSizeSuffix(scale, postfix, binaryPower);
		}

		//const string _formatDescription = "{0:original:2048} {1:size:2} {2:suff:K} {3:postfix:iB} {4:suffPostf:KiB}";

		/// <summary>
		/// f(2048, true, true) := 2 KiB
		/// f(2048, false, true) := 2 KB
		/// </summary>
		/// <param name="scale">1 (1 000, K) 2 (1 000 000, M) ...</param>
		/// <param name="postfix">3 K[ib] &lt;= here iB is a sample of a postfix</param>
		/// <returns></returns>
		static string GetSizeSuffix(int scale, string postfix)
		{
			return GetSizeSuffix(scale, postfix, true);
		}

		/// <summary>
		/// f(2048, true, true) := 2 KiB
		/// f(2048, false, true) := 2 KB
		/// </summary>
		/// <param name="scale">1 (1 000, K) 2 (1 000 000, M) ...</param>
		/// <param name="postfix">3 K[ib] &lt;= here iB is a sample of a postfix</param>
		/// <param name="binaryPower">Binary power (2^(x*10)) or decumal power (10^(x*3))</param>
		/// <returns></returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength")]
		static string GetSizeSuffix(int scale, string postfix, bool binaryPower)
		{
			var suff = GetSizeSuffix(scale);
			switch (suff)
			{
				case null: // unknown - generic format
					return binaryPower ? "*2^" + scale + "0 " + Resources.InfoSize_B : "*10^" + (3 + scale) + " " + Resources.InfoSize_B;
				case "": // no scale - just bytes
					return suff + Resources.InfoSize_B;
				default: // scale prefix (K M G...)
					return suff + postfix;
			}
		}

		/// <summary>
		/// f(2) := 'M'
		/// f(3) := 'G'
		/// </summary>
		static string GetSizeSuffix(int scale)
		{
			switch (scale)
			{
				case 0:
					return string.Empty;
				case 1:
					return Resources.InfoSize_K;
				case 2:
					return Resources.InfoSize_M;
				case 3:
					return Resources.InfoSize_G;
				case 4:
					return Resources.InfoSize_T;
				case 5:
					return Resources.InfoSize_P;
				case 6:
					return Resources.InfoSize_E;
				default:
					return null;
			}
		}

		public static InfoSize operator -(InfoSize a, InfoSize b)
		{
			return a._size - b._size;
		}

		public static InfoSize operator +(InfoSize a, InfoSize b)
		{
			return a._size + b._size;
		}

		public static InfoSize operator *(InfoSize a, int b)
		{
			return a._size * b;
		}

		public static InfoSize operator /(InfoSize a, int b)
		{
			return a._size / b;
		}

		public static bool operator ==(InfoSize a, InfoSize b)
		{
			return a.Equals(b);
		}

		public static bool operator !=(InfoSize a, InfoSize b)
		{
			return !(a == b);
		}

		public override bool Equals(object obj)
		{
			if (ReferenceEquals(null, obj))
			{
				return false;
			}
			if (obj.GetType() != typeof (InfoSize))
			{
				return false;
			}
			return Equals((InfoSize) obj);
		}

		public override int GetHashCode()
		{
			return _size.GetHashCode();
		}

		readonly static WeakValueCache<IFormatProvider, Regex> _rxCache = new WeakValueCache<IFormatProvider, Regex>(TimeSpan.FromDays(1));

		static Regex GetRegexForProvider(IFormatProvider provider)
		{
			if (provider == null)
			{
				provider = CultureInfo.CurrentCulture;
			}

			// culture-sensitive
			return _rxCache[provider, () =>
			{
				var nfi = (NumberFormatInfo)provider.GetFormat(typeof (NumberFormatInfo));
				if (nfi==null)
				{
					throw new Exception(@"NumberFormatInfo is unavailable for current IFormatProvider");
				}
				var chars = string.Join("", 
#if PocketPC
					_nativeDigits
#else
					nfi.NativeDigits
#endif
					) + nfi.NegativeSign + nfi.PositiveSign + nfi.NumberGroupSeparator + nfi.NumberDecimalSeparator;
				chars = Regex.Escape(chars).Replace("-", "\\-").Replace("^", "\\^"); // "-" and "^" should be escaped in groups
				return new Regex(string.Format(@"(?i)(?'n'[{0}]+)\s*(?'sectionName'\w*)", chars), RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.IgnoreCase);
			}]; 
		}

#if PocketPC
		static readonly string[] _nativeDigits = new[]
		{
			"0",
			"1",
			"2",
			"3",
			"4",
			"5",
			"6",
			"7",
			"8",
			"9",
		};
#endif

		static decimal ParseSizeString(string str, IFormatProvider provider, bool binaryPower)
		{
			var ms = GetRegexForProvider(provider).Matches(str);
			return ms.Cast<Match>().Sum(match => ParsePart(match, provider, binaryPower));
		}

		static bool IsNthScale(int scale, string scaleSpecifier, IFormatProvider provider)
		{
			return
				IsNthScale(GetScale(scale, provider), scaleSpecifier, provider) ||
				IsNthScale(GetScale(scale, null), scaleSpecifier, null) ||
				IsNthScale(GetScale(scale, CultureInfo.InvariantCulture), scaleSpecifier, CultureInfo.InvariantCulture)
				;
		}

		static string GetScale(int scale, IFormatProvider provider)
		{
			switch (scale)
			{
				case 10:
					return Resources.GetInfoSize_K(provider as CultureInfo);
				case 20:
					return Resources.GetInfoSize_M(provider as CultureInfo);
				case 30:
					return Resources.GetInfoSize_G(provider as CultureInfo);
				case 40:
					return Resources.GetInfoSize_T(provider as CultureInfo);
				case 50:
					return Resources.GetInfoSize_P(provider as CultureInfo);
				case 60:
					return Resources.GetInfoSize_E(provider as CultureInfo);
				default:
					return "{unknown}";
			}
		}

		static bool IsNthScale(string scaleSuffix, string scaleSpecifier, IFormatProvider provider)
		{
			var a = scaleSuffix;
			var b = scaleSuffix + Resources.GetInfoSize_B(provider as CultureInfo);
			var c = scaleSuffix + Resources.GetInfoSize_iB(provider as CultureInfo);

			const StringComparison options = StringComparison.CurrentCultureIgnoreCase;

			return a.Equals(scaleSpecifier, options) || (b.Equals(scaleSpecifier, options) || c.Equals(scaleSpecifier, options));
		}

		static decimal ParsePart(Match match, IFormatProvider provider, bool binaryPower)
		{
				var si = match.Groups["sectionName"].Value; // scale identifier
				decimal scale;
				var pow = binaryPower ? 1024M : 1000M;
				if (string.IsNullOrEmpty(si) || IsNthScale(string.Empty, si, provider))
				{
					scale = 1;
				}
				else if (IsNthScale(10, si, provider))
				{
					scale = pow;
				}
				else if (IsNthScale(20, si, provider))
				{
					scale = pow * pow;
				}
				else if (IsNthScale(30, si, provider))
				{
					scale = pow * pow * pow;
				}
				else if (IsNthScale(40, si, provider))
				{
					scale = pow * pow * pow * pow;
				}
				else if (IsNthScale(50, si, provider))
				{
					scale = pow * pow * pow * pow * pow;
				}
				else if (IsNthScale(60, si, provider))
				{
					scale = pow * pow * pow * pow * pow * pow;
				}
				else
				{
					throw new Exception("Can not parse SizeString: unknown scale identifer in " + match.Value);
				}

				decimal val;
				var n = match.Groups["n"].Value;

#if PocketPC
			val = decimal.Parse(n);
#else
				if (!decimal.TryParse(n, NumberStyles.Float | NumberStyles.AllowThousands, provider, out val))
				{
					throw new Exception("Can not parse SizeString: unknown number in " + match.Value);
				}
#endif
				return checked(Math.Round(val * scale));
		}

	}
}
